package cn.ty.jpa.wrapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

public class SpecificationWrapper<T> {
    private Root<T> root;
    private CriteriaQuery<?> query;
    private CriteriaBuilder builder;
    private List<Predicate> predicates = new ArrayList<>();
    private static final String SEPARATOR = ".";

    public SpecificationWrapper(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        this.root = root;
        this.query = query;
        this.builder = builder;
    }

    public SpecificationWrapper<T> or(Consumer<SpecificationWrapper<T>> action) {
        return this.newWrapper(false, action);
    }

    public SpecificationWrapper<T> and(Consumer<SpecificationWrapper<T>> action) {
        return this.newWrapper(true, action);
    }

    public SpecificationWrapper<T> newWrapper(boolean isConjunction, Consumer<SpecificationWrapper<T>> action) {
        SpecificationWrapper<T> specification = new SpecificationWrapper<>(this.root, this.query, this.builder);
        CriteriaBuilder newBuilder = specification.getBuilder();
        Predicate predicate = isConjunction ? newBuilder.conjunction() : newBuilder.disjunction();
        action.accept(specification);
        predicate.getExpressions().addAll(specification.getPredicates());
        this.predicates.add(predicate);
        return this;
    }

    public SpecificationWrapper<T> isNull(boolean condition, String name) {
        return condition ? this.isNull(name) : this;
    }

    public SpecificationWrapper<T> isNull(String name) {
        return this.handle(name, (e) -> {
            this.isNull((Expression)e);
        });
    }

    public SpecificationWrapper<T> isNull(Expression<?> x) {
        this.predicates.add(this.builder.isNull(x));
        return this;
    }

    public SpecificationWrapper<T> isNotNull(boolean condition, String name) {
        return condition ? this.isNotNull(name) : this;
    }

    public SpecificationWrapper<T> isNotNull(String name) {
        return this.handle(name, (e) -> {
            this.isNotNull((Expression)e);
        });
    }

    public SpecificationWrapper<T> isNotNull(Expression<?> x) {
        this.predicates.add(this.builder.isNotNull(x));
        return this;
    }

    public SpecificationWrapper<T> eq(boolean condition, String name, Object value) {
        return condition ? this.eq(name, value) : this;
    }

    public SpecificationWrapper<T> eq(String name, Object value) {
        return this.handle(name, (e) -> {
            this.eq((Expression)e, value);
        });
    }

    public SpecificationWrapper<T> eq(Expression<?> x, Object value) {
        this.predicates.add(this.builder.equal(x, value));
        return this;
    }

    public SpecificationWrapper<T> ne(boolean condition, String name, Object value) {
        return condition ? this.ne(name, value) : this;
    }

    public SpecificationWrapper<T> ne(String name, Object value) {
        return this.handle(name, (e) -> {
            this.ne((Expression)e, value);
        });
    }

    public SpecificationWrapper<T> ne(Expression<?> x, Object value) {
        this.predicates.add(this.builder.notEqual(x, value));
        return this;
    }

    public SpecificationWrapper<T> like(boolean condition, String name, String value) {
        return condition ? this.like(name, value) : this;
    }

    public SpecificationWrapper<T> like(String name, String value) {
        return this.handle(name, (e) -> {
            this.like((Expression)e, value);
        });
    }

    public SpecificationWrapper<T> like(Expression<String> path, String value) {
        this.predicates.add(this.builder.like(path, value));
        return this;
    }

    public SpecificationWrapper<T> startingWith(boolean condition, String name, String value) {
        return condition ? this.startingWith(name, value) : this;
    }

    public SpecificationWrapper<T> startingWith(String name, String value) {
        this.like(name, value + "%");
        return this;
    }

    public SpecificationWrapper<T> endingWith(boolean condition, String name, String value) {
        return condition ? this.endingWith(name, value) : this;
    }

    public SpecificationWrapper<T> endingWith(String name, String value) {
        this.like(name, "%" + value);
        return this;
    }

    public SpecificationWrapper<T> contains(boolean condition, String name, String value) {
        return condition ? this.contains(name, value) : this;
    }

    public SpecificationWrapper<T> contains(String name, String value) {
        this.like(name, "%" + value + "%");
        return this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> ge(boolean condition, String name, Y value) {
        return condition ? this.ge(name, value) : this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> ge(String name, Y value) {
        return this.handle(name, (e) -> {
            this.ge((Expression)e, value);
        });
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> ge(Expression<? extends Y> path, Y value) {
        this.predicates.add(this.builder.greaterThanOrEqualTo(path, value));
        return this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> le(boolean condition, String name, Y value) {
        return condition ? this.le(name, value) : this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> le(String name, Y value) {
        return this.handle(name, (e) -> {
            this.le((Expression)e, value);
        });
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> le(Expression<? extends Y> path, Y value) {
        this.predicates.add(this.builder.lessThanOrEqualTo(path, value));
        return this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> gt(boolean condition, String name, Y value) {
        return condition ? this.gt(name, value) : this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> gt(String name, Y value) {
        return this.handle(name, (e) -> {
            this.gt((Expression)e, value);
        });
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> gt(Expression<? extends Y> path, Y value) {
        this.predicates.add(this.builder.greaterThan(path, value));
        return this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> lt(boolean condition, String name, Y value) {
        return condition ? this.lt(name, value) : this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> lt(String name, Y value) {
        return this.handle(name, (e) -> {
            this.lt((Expression)e, value);
        });
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> lt(Expression<? extends Y> path, Y value) {
        this.predicates.add(this.builder.lessThan(path, value));
        return this;
    }

    public SpecificationWrapper<T> in(boolean condition, String name, Object... value) {
        return condition ? this.in(name, value) : this;
    }

    public SpecificationWrapper<T> in(String name, Object... value) {
        return this.handle(name, (e) -> {
            this.in((Expression)e, (Object[])value);
        });
    }

    public SpecificationWrapper<T> in(boolean condition, String name, Collection<?> value) {
        return condition ? this.in(name, value) : this;
    }

    public SpecificationWrapper<T> in(String name, Collection<?> value) {
        return this.in(name, value.toArray());
    }

    public <U> SpecificationWrapper<T> in(Expression<? extends U> expression, Object... value) {
        this.predicates.add(expression.in(value));
        return this;
    }

    public SpecificationWrapper<T> notIn(boolean condition, String name, Collection<?> value) {
        return condition ? this.notIn(name, value) : this;
    }

    public SpecificationWrapper<T> notIn(String name, Collection<?> value) {
        return this.handle(name, (e) -> {
            this.notIn((Expression)e, (Object[])value.toArray());
        });
    }

    public SpecificationWrapper<T> notIn(boolean condition, String name, Object... value) {
        return condition ? this.notIn(name, value) : this;
    }

    public SpecificationWrapper<T> notIn(String name, Object... value) {
        return this.handle(name, (e) -> {
            this.notIn((Expression)e, (Object[])value);
        });
    }

    public <U> SpecificationWrapper<T> notIn(Expression<? extends U> expression, Object... value) {
        this.predicates.add(expression.in(value).not());
        return this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> between(boolean condition, String name, Y start, Y end) {
        return condition ? this.between(name, start, end) : this;
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> between(String name, Y start, Y end) {
        return this.handle(name, (e) -> {
            this.between((Expression)e, start, end);
        });
    }

    public <Y extends Comparable<? super Y>> SpecificationWrapper<T> between(Expression<? extends Y> path, Y start, Y end) {
        this.predicates.add(this.builder.between(path, start, end));
        return this;
    }

    public <U> Join<T, U> leftJoin(String fieldName) {
        return this.root.join(fieldName, JoinType.LEFT);
    }

    public SpecificationWrapper<T> handle(String name, Consumer<Path> action) {
        Path path;
        if (name.contains(".")) {
            String[] arr = name.split("\\.");
            path = this.leftJoin(arr[0]).get(arr[1]);
        } else {
            path = this.root.get(name);
        }

        action.accept(path);
        return this;
    }

    public Root<T> getRoot() {
        return this.root;
    }

    public CriteriaQuery<?> getQuery() {
        return this.query;
    }

    public CriteriaBuilder getBuilder() {
        return this.builder;
    }

    public List<Predicate> getPredicates() {
        return this.predicates;
    }

    public void setRoot(final Root<T> root) {
        this.root = root;
    }

    public void setQuery(final CriteriaQuery<?> query) {
        this.query = query;
    }

    public void setBuilder(final CriteriaBuilder builder) {
        this.builder = builder;
    }

    public void setPredicates(final List<Predicate> predicates) {
        this.predicates = predicates;
    }

    public SpecificationWrapper() {
    }
}
